package jamirr;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.*;

class ArgumentImpl<T> extends AnnotatedElementImpl implements Argument<T> {

    private final Class<T> type;
    private final String name;
    private final Map<String, Argument<?>> typeParameters;
    private final Argument<?>[] typeParameterArray;
    private final boolean isTypeVar;
    private String namePrecalculated;

    /**
     * @param type                The type
     * @param name                The name
     * @param declaredAnnotations The declared annotations
     * @param genericTypes        The generic types
     */
    public ArgumentImpl(Class<T> type, String name, AnnotationMirror[] declaredAnnotations, Argument<?>... genericTypes) {
        this(type,
                name,
                declaredAnnotations,
                genericTypes != null && genericTypes.length > 0 ? initializeTypeParameters(genericTypes) : Collections.emptyMap(),
                genericTypes
        );
    }

    /**
     * @param type                The type
     * @param declaredAnnotations The declared annotations
     * @param genericTypes        The generic types
     */
    public ArgumentImpl(Class<T> type, AnnotationMirror[] declaredAnnotations, Argument<?>... genericTypes) {
        this(type,
                null,
                declaredAnnotations,
                genericTypes != null && genericTypes.length > 0 ? initializeTypeParameters(genericTypes) : Collections.emptyMap(),
                genericTypes
        );
    }

    /**
     * @param type                The type
     * @param name                The name
     * @param declaredAnnotations The declared annotations
     * @param typeParameters      The map of parameters
     * @param typeParameterArray  The array of arguments
     */
    public ArgumentImpl(Class<T> type, String name, AnnotationMirror[] declaredAnnotations, Map<String, Argument<?>> typeParameters, Argument<?>[] typeParameterArray) {
        this(type, name, declaredAnnotations, typeParameters, typeParameterArray, false);
    }

    /**
     * @param type                The type
     * @param name                The name
     * @param declaredAnnotations The declared annotations
     * @param isTypeVariable      Is this argument a type variable
     * @param genericTypes        The generic types
     */
    public ArgumentImpl(Class<T> type, String name, AnnotationMirror[] declaredAnnotations, boolean isTypeVariable, Argument<?>... genericTypes) {
        this(type,
                name,
                declaredAnnotations,
                genericTypes != null && genericTypes.length > 0 ? initializeTypeParameters(genericTypes) : Collections.emptyMap(),
                genericTypes,
                isTypeVariable
        );
    }

    /**
     * @param type                The type
     * @param name                The name
     * @param declaredAnnotations The declared annotations
     * @param typeParameters      The map of parameters
     * @param typeParameterArray  The array of arguments
     * @param isTypeVariable      Is the argument a type variable
     */
    protected ArgumentImpl(Class<T> type, String name, AnnotationMirror[] declaredAnnotations, Map<String, Argument<?>> typeParameters, Argument<?>[] typeParameterArray, boolean isTypeVariable) {
        super(declaredAnnotations);
        this.type = Objects.requireNonNull(type, "Type cannot be null");
        this.name = name;
        this.typeParameters = typeParameters;
        this.typeParameterArray = typeParameterArray;
        this.isTypeVar = isTypeVariable;
    }

    /**
     * @param type                The type
     * @param name                The name
     * @param declaredAnnotations The declared annotations
     */
    @SuppressWarnings("unchecked")
    public ArgumentImpl(Type type, String name, AnnotationMirror[] declaredAnnotations) {
        super(declaredAnnotations);
        if (type == null) {
            type = getClass().getGenericSuperclass();
            if (type instanceof ParameterizedType parameterizedType) {
                type = parameterizedType.getActualTypeArguments()[0];
            } else {
                throw new IllegalArgumentException(type + " is not parameterized");
            }
        }
        if (type instanceof Class) {
            this.type = (Class<T>) type;
            this.typeParameterArray = ZERO_ARGUMENTS;
        } else if (type instanceof ParameterizedType parameterizedType) {
            this.type = (Class<T>) parameterizedType.getRawType();
            TypeVariable<Class<T>>[] params = this.type.getTypeParameters();
            Type[] paramValues = parameterizedType.getActualTypeArguments();
            typeParameterArray = new Argument[params.length];
            for (int i = 0; i < params.length; i++) {
                TypeVariable<?> param = params[i];
                Type value = paramValues[i];
                typeParameterArray[i] = new ArgumentImpl<>(value, param.getName(), null);
            }
        } else {
            throw new IllegalArgumentException(type.getClass().getSimpleName() + " types are not supported");
        }
        this.name = name;
        this.typeParameters = initializeTypeParameters(this.typeParameterArray);
        this.isTypeVar = false;
    }

    @Override
    public boolean isTypeVariable() {
        return isTypeVar;
    }

    @SuppressWarnings("rawtypes")
    @Override
    public Argument[] getTypeParameters() {
        return Objects.requireNonNullElse(typeParameterArray, ZERO_ARGUMENTS);
    }

    @Override
    public Map<String, Argument<?>> getTypeVariables() {
        return this.typeParameters;
    }

    @Override
    public Class<T> getType() {
        return type;
    }

    @Override
    public String getName() {
        if (name != null) {
            return name;
        }
        if (namePrecalculated == null) {
            namePrecalculated = Utils.uncapitalize(type.getSimpleName());
        }
        return namePrecalculated;
    }

    @Override
    public String toString() {
        if (this.name == null) {
            return getType().getSimpleName();
        } else {
            return getType().getSimpleName() + " " + getName();
        }
    }

    @Override
    public boolean equalsType(Argument<?> o) {
        if (this == o) {
            return true;
        }
        if (o == null) {
            return false;
        }
        return Objects.equals(type, o.getType()) &&
                Objects.equals(typeParameters, o.getTypeVariables());
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof ArgumentImpl<?> that)) {
            return false;
        }
        return Objects.equals(type, that.type) &&
                Objects.equals(getName(), that.getName()) &&
                Objects.equals(typeParameters, that.typeParameters);
    }

    @Override
    public int typeHashCode() {
        return Utils.hash(type, typeParameters);
    }

    @Override
    public int hashCode() {
        return Utils.hash(type, getName(), typeParameters);
    }

    public static final Set<String> CONTAINER_TYPES = Utils.setOf(
            List.class.getName(),
            Set.class.getName(),
            Collection.class.getName(),
            Queue.class.getName(),
            SortedSet.class.getName(),
            Deque.class.getName(),
            Vector.class.getName(),
            ArrayList.class.getName()
    );

    private static Map<String, Argument<?>> initializeTypeParameters(Argument<?>[] genericTypes) {
        Map<String, Argument<?>> typeParameters;
        if (genericTypes != null && genericTypes.length > 0) {
            typeParameters = Utils.newLinkedHashMap(genericTypes.length);
            for (Argument<?> genericType : genericTypes) {
                typeParameters.put(genericType.getName(), genericType);
            }
        } else {
            typeParameters = Collections.emptyMap();
        }
        return typeParameters;
    }

}
